Skip to main content

media_pp\platform\windows\wasapi/
device.rs

1//! WASAPI endpoint enumeration and COM apartment management.
2
3use windows::{
4    Win32::{
5        Devices::FunctionDiscovery::PKEY_Device_FriendlyName,
6        Foundation::RPC_E_CHANGED_MODE,
7        Media::Audio::{
8            DEVICE_STATE_ACTIVE, IMMDevice, IMMDeviceEnumerator, MMDeviceEnumerator, eCapture,
9            eConsole, eRender,
10        },
11        System::{
12            Com::{
13                CLSCTX_ALL, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx, CoUninitialize,
14                STGM_READ,
15                StructuredStorage::{PROPVARIANT, PropVariantClear},
16            },
17            Variant::VT_LPWSTR,
18        },
19        UI::Shell::PropertiesSystem::IPropertyStore,
20    },
21    core::HSTRING,
22};
23
24/// Which direction a WASAPI endpoint flows.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum WasapiDeviceKind {
27    /// Speakers, headphones, HDMI audio, or another playback endpoint.
28    Render,
29    /// A microphone or another recording endpoint.
30    Capture,
31}
32
33/// One active WASAPI endpoint.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct WasapiDevice {
36    /// Opaque `IMMDevice::GetId` value.
37    pub id: String,
38    /// Human-readable endpoint name, falling back to `id` when Windows
39    /// does not expose a friendly name.
40    pub name: String,
41    pub kind: WasapiDeviceKind,
42    /// Whether this was the default console endpoint for `kind` when it
43    /// was enumerated.
44    pub is_default: bool,
45}
46
47/// Balances one successful `CoInitializeEx` on the current thread.
48pub(crate) struct ComApartment {
49    uninitialize: bool,
50}
51
52impl ComApartment {
53    pub(crate) fn new() -> windows::core::Result<Self> {
54        let result = unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) };
55        if result == RPC_E_CHANGED_MODE {
56            // The caller already initialized this thread as STA. COM is
57            // available and WASAPI works there; only the apartment model
58            // cannot be changed, and this call must not be balanced.
59            return Ok(Self {
60                uninitialize: false,
61            });
62        }
63        result.ok()?;
64        Ok(Self { uninitialize: true })
65    }
66}
67
68impl Drop for ComApartment {
69    fn drop(&mut self) {
70        if self.uninitialize {
71            unsafe { CoUninitialize() };
72        }
73    }
74}
75
76pub(crate) fn list_devices(
77    kind_filter: Option<WasapiDeviceKind>,
78) -> windows::core::Result<Vec<WasapiDevice>> {
79    let _apartment = ComApartment::new()?;
80    let enumerator: IMMDeviceEnumerator =
81        unsafe { CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)? };
82
83    let kinds: &[(windows::Win32::Media::Audio::EDataFlow, WasapiDeviceKind)] = match kind_filter {
84        Some(WasapiDeviceKind::Render) => &[(eRender, WasapiDeviceKind::Render)],
85        Some(WasapiDeviceKind::Capture) => &[(eCapture, WasapiDeviceKind::Capture)],
86        None => &[
87            (eRender, WasapiDeviceKind::Render),
88            (eCapture, WasapiDeviceKind::Capture),
89        ],
90    };
91
92    let mut devices = Vec::new();
93    for &(dataflow, kind) in kinds {
94        let default_id = unsafe { enumerator.GetDefaultAudioEndpoint(dataflow, eConsole) }
95            .ok()
96            .and_then(|device| unsafe { device.GetId() }.ok())
97            .and_then(|id| unsafe { id.to_string() }.ok());
98
99        let collection = unsafe { enumerator.EnumAudioEndpoints(dataflow, DEVICE_STATE_ACTIVE)? };
100        let count = unsafe { collection.GetCount()? };
101        for index in 0..count {
102            let device = unsafe { collection.Item(index)? };
103            let Some(id) = unsafe { device.GetId() }
104                .ok()
105                .and_then(|id| unsafe { id.to_string() }.ok())
106            else {
107                continue;
108            };
109            let name = device_friendly_name(&device).unwrap_or_else(|| id.clone());
110            let is_default = default_id.as_deref() == Some(id.as_str());
111            devices.push(WasapiDevice {
112                id,
113                name,
114                kind,
115                is_default,
116            });
117        }
118    }
119    Ok(devices)
120}
121
122pub(crate) fn open_device(id: &str) -> windows::core::Result<IMMDevice> {
123    let enumerator: IMMDeviceEnumerator =
124        unsafe { CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)? };
125    let id = HSTRING::from(id);
126    unsafe { enumerator.GetDevice(&id) }
127}
128
129fn device_friendly_name(device: &IMMDevice) -> Option<String> {
130    unsafe {
131        let store: IPropertyStore = device.OpenPropertyStore(STGM_READ).ok()?;
132        let mut variant: PROPVARIANT = store.GetValue(&PKEY_Device_FriendlyName).ok()?;
133        let name = property_variant_to_string(&variant);
134        let _ = PropVariantClear(&mut variant);
135        name
136    }
137}
138
139fn property_variant_to_string(variant: &PROPVARIANT) -> Option<String> {
140    unsafe {
141        if variant.Anonymous.Anonymous.vt != VT_LPWSTR {
142            return None;
143        }
144        variant
145            .Anonymous
146            .Anonymous
147            .Anonymous
148            .pwszVal
149            .to_string()
150            .ok()
151    }
152}